import wx
import sys

from traceback import print_exc
from operator import itemgetter
from threading import Event, Timer
from time import time, sleep
from binascii import unhexlify

from BitTornado.clock import clock
from BitTornado.download_bt1 import BT1Download

from LMG.Utility.constants import *
from LMG.Utility.helpers import getfreespace
from LMG.GUI.Base.guiupdate import SafeInvocation

################################################################

class ABCEngine(wx.EvtHandler, SafeInvocation):
    """
    This is the part of a torrent object created while it is
    active.  It handles processing of statistics and information
    from the BitTornado core.
    """
    def __init__(self, torrent, myid):
        wx.EvtHandler.__init__(self)
        SafeInvocation.__init__(self)
        self.doneflag = Event()
        
        self.torrent = torrent

        self.downsize = { 'old' : self.torrent.files.downsize, 
                          'new' : 0.0,
                          'prev': 0.0 }
        self.upsize = { 'old' : self.torrent.files.upsize, 
                        'new' : 0.0,
                        'prev': 0.0 }
        
        self.spewwait = time()

        self.reannouncelast = 0

        self.timers = { 'lastupload': time(), 
                        'lastdownload': time(),
                        'infrequent': None }

        self.btstatus = _('waiting')
        self.progress = self.torrent.files.progress
        self.sizeDone = self.torrent.files.sizeDone
        
        self.eta = None
        self.elapsed = ""
        self.rate = { "down" : 0.0, 
                      "up" : 0.0 }

        self.numpeers = 0
        self.numseeds = 0

        self.numconnections = 0
        self.numcopies = None
        self.peeravg = None
        self.totalspeed = 0.0
        self.curtracker = None
        self.numerrortracker=0
        self.lasterrortracker=0
        
        # What is the current rate set at?
        self.ratesetting = {}
        self.ratesetting['up'] = 0.0
        self.ratesetting['down'] = 0.0
        
        self.seedingtimelastcheck = None

        # To compute mean over several last values of uprate/downrate        
        self.pastrate = { "down" : [0.0] * 20, 
                          "up" : [0.0] * 20 }
        self.meanrate = { "down" : 0.0, 
                          "up" : 0.0 }
                     
        # Keep track of if we have any connections
        self.hasConnections = False
        
        # New stuff....

        self.controller = utility.controller
        
        self.waiting = True
        self.checking = False
        self.working = False
        self.seed = False
        self.closed = False

        self.hadextern = False

        
        self.start_time = 0
        
        self.status_err = ['']
        self.status_errtime = 0
        self.status_done = 0.0
                
        self.response = self.torrent.metainfo
        hash = unhexlify(self.torrent.infohash)

        self.rawserver = self.controller.handler.newRawServer(hash, self.doneflag)
        
        btconfig = utility.getBTParams()

        self.dow = BT1Download(self.display, 
                               self.finished,
                               self.unfinished,
                               self.error, 
                               self.error, # Use this rather than controller.exchandler
                               self.info,
                               self.doneflag, 
                               btconfig,
                               self.response, 
                               hash, 
                               myid, 
                               self.rawserver, 
                               self.controller.listen_port,
                               utility.configdir,
                               private = self.torrent.private,
                               dht = utility.dht,
                               ipfilter = utility.ipfilter.filter)

    ####################################
    # BEGIN NEW STUFF (FOR SINGLE PORT)
    ####################################

    def start(self):
        self.start_time = clock()
                
        # Delete the cache information if doing a hashcheck
        if self.torrent.status.value == STATUS_HASHCHECK:
            self.dow.appdataobj.deleteTorrentData(self.dow.infohash)
               
        if not self.dow.saveAs(self.saveAs):
            self.shutdown()
            return

        self._hashcheckfunc = self.dow.initFiles()

        if not self._hashcheckfunc:
            self.shutdown()
            return

        self.controller.hashchecksched(self.torrent)

    def hashcheck_start(self, donefunc):
        if self.is_dead():
            self.shutdown()
            return
        self.waiting = False
        self.checking = True
        
        # Start infrequent tasks
        self._InfrequentTasks()
        
        self._hashcheckfunc(donefunc)

    def hashcheck_callback(self):
        self.checking = False
        
        if self.torrent.status.value == STATUS_HASHCHECK \
           and not self.torrent.actions.oldstatus in [ STATUS_ACTIVE, STATUS_PAUSE, STATUS_SUPERSEED ]:
            obtained, desired = self.dow.storagewrapper.get_stats()
            if desired > 0:
                self.onUpdateProgress(float(obtained)/desired, obtained, save = True)
            else:
                self.onUpdateProgress(1.0, obtained, save = True)            
            self.shutdown(True)
            return

        if self.is_dead():
            self.shutdown()
            return

        if not self.dow.startEngine(feedbackfunc = self.nomatadata_feedback,
                                    finishfunc = self.nometadata_finish):
            self.shutdown()
            return
        
        self.dow.startRerequester()
        self.statsfunc = self.dow.startStats()

        if self.torrent.status.value != STATUS_HASHCHECK:
            self.torrent.status.updateStatus(STATUS_ACTIVE)

        self.torrent.files.setFilePriorities()
        self.torrent.connection.setMaxInitiate()
        
        # Set the spew flag if the detail window is shown
        if utility.window.details.getTorrent() == self.torrent:
            self.dow.spewflag.set()

        self.rawserver.start_listening(self.dow.getPortHandler())
        self.working = True

    def nomatadata_feedback(self, total, done):
        self.torrent.files.size_metadataless = total
        self.torrent.files.sizeDone_metadataless = done
        self.torrent.updateColumns([COL_SIZE, COL_DONESIZE])
        
    def nometadata_finish(self, rawinfo):
        self.torrent.InitializeInfo(rawinfo)
        
    def shutdown(self, fromhash = False):
        # Remove from the active torrents list
        try:
            del utility.torrents["active"][self.torrent]
        except:
            pass
        
        # Cancel timer
        try:
            if self.timers['infrequent'] is not None:
                self.timers['infrequent'].cancel()
        except:
            pass

        if self.closed:
            return
        self.doneflag.set()
        
        
        try:
            self.rawserver.shutdown()
        except:
            print_exc()
            pass

        try:
            self.dow.shutdown()
        except:
            print_exc()
            pass
        
        self.waiting = False
        self.checking = False
        self.working = False
        self.closed = True

        if fromhash:
            self.controller.hashcheck_was_stopped(self.torrent)
        else:
            self.controller.was_stopped(self.torrent)

        # Clear Details window
        try:
            self._clearDetailWindow()
        except:
            pass

    def display(self, activity = None, fractionDone = None):
        # really only used by StorageWrapper now
        self._setActivity(activity)
        if fractionDone is not None:
            self.status_done = float(fractionDone)

    def error(self, msg):
        if self.doneflag.isSet():
            self.shutdown()
        self.status_err.append(msg)
        self.status_errtime = clock()
        self.invokeLater(self._errormsg, [msg])

    def info(self, msg):
        self.invokeLater(self._infomsg, [msg])
  
    def saveAs(self, name, length, saveas, isdir):
        return self.torrent.files.dest
        
    def done(self, event = None):
        self.torrent.set()
        
    def is_dead(self):
        return self.doneflag.isSet()
        
    ####################################
    # END NEW STUFF (FOR SINGLE PORT)
    ####################################
        
    def _setActivity(self, activity):
        if activity is not None:
            activities = { "checking existing data": _('checking data'), 
                           "allocating disk space": _('allocating space'), 
                           "moving data": _('moving data') }
            self.btstatus = activities.get(activity, activity)

    def onUpdateProgress(self, fractionDone, sizeDone, save = False):
        if fractionDone is not None and not self.seed:
            self.progress = (float(fractionDone) * 100)

        if sizeDone is not None:
            self.sizeDone = float(sizeDone)

        if save:
            self.torrent.files.progress = self.progress
            self.torrent.files.sizeDone = self.sizeDone
            self.invokeLater(self._onUpdateProgress)
        
    def _onUpdateProgress(self):
        self.torrent.updateColumns([COL_PROGRESS,
                                    COL_BTSTATUS,
                                    COL_DONESIZE])

    def onUpdateStatus(self, fractionDone = None, 
            timeEst = None, downRate = None, upRate = None, 
            activity = None, statistics = None, spew = None, sizeDone = None,
            **kws):

        self.invokeLater(self._updateStatus, [fractionDone, timeEst, downRate, upRate, activity, statistics, spew, sizeDone])
        
    def _updateStatus(self, fractionDone = None, timeEst = None, downRate = None, upRate = None,
                      activity = None, statistics = None, spew = None, sizeDone = None,):
        # Just in case a torrent was finished
        # but now isn't
        self.torrent.status.completed = self.seed
            
        # Get scrape data every 20 minutes
        #############################################
        if utility.config.Read('scrape', "boolean"):
            self.torrent.actions.scrape()
       
        # Get Display Data
        #############################################
        self.onUpdateProgress(fractionDone, sizeDone)
            
        self._setActivity(activity)

        if timeEst is not None:
            self.eta = timeEst
        else:
            self.eta = -1
            
        self.elapsed = utility.eta_value(clock() - self.start_time)
        
        if self.torrent.status.value != STATUS_PAUSE:
            if not self.seed and downRate is not None:
                self.rate['down'] = float(downRate)
                if self.rate['down'] != 0.0:
                    self.timers['lastdownload'] = time()
            else:
                self.rate['down'] = 0.0
                       
            if upRate is not None:
                self.rate['up'] = float(upRate)

                self.pastrate['up'].append(self.rate['up'])
                self.pastrate['up'].pop(0)

                if self.rate['up'] != 0.0:
                    self.timers['lastupload'] = time()

                # Compute mean uprate

                total = sum(self.pastrate['up'])
                self.meanrate['up'] = total / 20
            else:
                self.rate['up'] = 0.0

        if statistics is not None:
            self.hadextern  = bool(statistics.external_connection_made)
            self.numpeers   = statistics.numPeers
            self.numcopies  = statistics.numCopies
            self.peeravg    = statistics.percentDone
            self.curtracker = statistics.trackerInfo[0]

            # Update download, upload, and progress
            self.downsize['prev'] = self.downsize['new']
            self.downsize['new'] = float(statistics.downTotal)
            if utility.config.Read('maxdownloadvolume', "int") > 0:
                utility.queue.volume['down'] += int(self.downsize['new'] - self.downsize['prev'])
            self.upsize['prev'] = self.upsize['new']
            self.upsize['new'] = float(statistics.upTotal)
            if utility.config.Read('maxuploadvolume', "int") > 0:    
                utility.queue.volume['up'] += int(self.upsize['new'] - self.upsize['prev'])
            self.torrent.files.updateProgress()
            self.totalspeed = float(statistics.torrentRate)
            self.numconnections = statistics.numPeers

            if not self.seed:
                self.numseeds = statistics.numSeeds
                self.numconnections += statistics.numSeeds
            else:
                self.numseeds = statistics.numOldSeeds
        else:
            self.peeravg = None
            self.numcopies = None
            self.curtracker = None
        
        if self.seed:
            self._countSeedingTime()
            if self.torrent.status.isDoneUploading():
                self._TerminateUpload()
            self.torrent.updateColumns([COL_SEEDING_TIME])

        # Update text strings
        self.torrent.updateColumns([COL_PROGRESS,
                                    COL_BTSTATUS, 
                                    COL_ETA, 
                                    COL_DLSPEED, 
                                    COL_ULSPEED, 
                                    COL_MESSAGE,
                                    COL_DONESIZE])
        if statistics is not None:
            # Share Ratio, #Seed, #Peer, #Copies, #Peer Avg Progress,
            # Download Size, Upload Size, Total Speed
            self.torrent.updateColumns([COL_RATIO, 
                                        COL_DLSIZE, 
                                        COL_ULSIZE,
                                        COL_RESOURCES])

        # Update progress in details window
        if utility.window.details.getTorrent() == self.torrent:
            self._updateDetailWindow(statistics, spew)
        
    def _updateDetailWindow(self, statistics = None, spew = None):
        #####################################################
        # Detail Window display part
        #####################################################
        try:
            # Update file progress
            self.torrent.files.updateFileProgress(statistics)
            
            # Update spew list
            self._updateSpewList(spew)
    
            # Update other window parts
            detailwin = utility.window.details
    
            if statistics is not None and detailwin.IsShown() \
               and detailwin.networkPage.IsShown() and utility.frame.GUIupdate:
                detailwin.updateNetwork('transfer')
                detailwin.updateNetwork('tracker', statistics.trackerInfo)
                detailwin.updateNetwork('storage', [statistics])
                
        except wx.PyDeadObjectError:
            # Just in case the window gets destroyed first
            pass
        
    def _updateSpewList(self, spew = None):        
        detailwin = utility.window.details
        if spew is None or detailwin.getTorrent() != self.torrent:
            # No spew information
            return
        elif not detailwin.peersPage.IsShown() or not utility.frame.GUIupdate:
            # No no window to display to
            return
        elif (time() - self.spewwait < 1.5):
            # Not enough time
            return
        

        # Totals info
        if self.peeravg is not None:
            peeravg = '%.1f%%' % self.peeravg
        else:
            peeravg = ""
        totalspeed = utility.speed_format(self.totalspeed, truncate = 0)
        
        spew.insert(0, {'uprate': self.rate['up'], 'downrate': self.rate['down'],
                        'utotal': self.upsize['new'], 'dtotal': self.downsize['new'],
                        'completed': peeravg, 'speed': totalspeed})
        
        detailwin.updatePeers(spew)
        
        self.spewwait = time()

    def _clearDetailWindow(self):
        detailwin = utility.window.details
        if detailwin.getTorrent() == self.torrent:
            detailwin.updatePeers()
            detailwin.updateNetwork('transfer')
            detailwin.updateNetwork('tracker', (None,'',''))
            detailwin.updateNetwork('storage')
        
    def _InfrequentTasks(self):
        """
        Do things that don't need to be done on every pass through updateStatus
        """
        try:
            if self.timers['infrequent'] is not None:
                self.timers['infrequent'].cancel()
        except:
            pass
        
        self.invokeLater(self._CheckTimeouts, [])
        self.invokeLater(self._CheckDiskSpace, [])
        
        # Should check diskspace more frequently
        # while in the "allocating" stage
        if self.waiting or self.checking:
            nextcheck = 2
        else:
            nextcheck = 30
        
        self.timers['infrequent'] = Timer(nextcheck, self._InfrequentTasks)
        self.timers['infrequent'].start()
    
    def _CheckDiskSpace(self):
        """
        Check to make sure that there's free diskspace
        """
        threshold = utility.config.Read('diskfullthreshold', "int")
        
        # Disk checking is disabled
        if threshold == 0:
            return
        
        # See how much more space the torrent needs
        spaceneeded = self.torrent.files.getSpaceNeeded()
        
        # Don't need to worry if the torrent already has
        # as much space as it needs
        if spaceneeded == 0L:
            return
            
        dest = self.torrent.files.getProcDest(pathonly = True)
        if dest is None:
            # Don't bother checking for space until the
            # destination path exists
            return
            
        spaceleft = getfreespace(dest)
        if spaceleft is False:
            utility.config.Write('diskfullthreshold', "0")
            dialog = wx.MessageDialog(None, 
                                      _('Free Disk Space check failed'), 
                                      _('Error'), 
                                      wx.ICON_ERROR)
            dlg.ShowModal()
            dlg.Destroy()
            return

        if spaceleft < long((2**20) * threshold):
            message = _('Disk is full') + \
                      " (" + utility.size_format(spaceleft) + ")"
            self._errormsg(message)
            self.actionhandlerCallback(STATUS_STOP, self.torrent)
    
    def _CheckTimeouts(self):
        """
        See if there's been a timeout
        """
        # Check to see if we need to check for timeouts
        if not self.torrent.connection.timeout or self.torrent.status.value == STATUS_PAUSE:
            return
        
        # Check no download transfer in 30 mins
        # (when this torrent is leeching torrent)
        ##########################################
        if not self.seed:
            timeoutdownload = utility.config.Read('timeoutdownload')
            if (timeoutdownload != 'oo'
                and (time() - self.timers['lastdownload']) > (float(timeoutdownload)*60)):
                self._ReducePrioandForceQueue()
                return

        # Check no upload transfer in 1 hour
        # (when this torrent is seeding torrent)
        ##########################################
        else:
            timeoutupload = utility.config.Read('timeoutupload')
            if ((timeoutupload != 'oo')
                and (time() - self.timers['lastupload']) > (float(timeoutupload)*3600)):
                self._ReducePrioandForceQueue()
                return

    def _errormsg(self, message):
        errors = {"problem connecting to tracker": _('problem connecting to tracker'), 
                  "rejected by tracker": _('rejected by tracker'), 
                  "bad data from tracker": _('bad data from tracker') }
        
        try:
            trackererror = False

            for error in errors:
                index = message.lower().find(error)
                if index != -1:
                    oldlen = len(error)
                    message = message[:index] + errors[error] + message[index + oldlen:]
                    trackererror = True
                    break
            if trackererror:
                currenttime = time()
                if self.lasterrortracker == 0:
                    self.lasterrortracker = currenttime

                if (currenttime - self.lasterrortracker) < 120: #error with in 2 mins
                    self.numerrortracker += 1
                else:
                    self.numerrortracker = 0
                self.lasterrortracker = currenttime
        except:
            pass
        
        self.torrent.changeMessage(message, "error")

        # If failed connecting tracker in parameter 'timeouttracker' mins
        # reduce its priority and force to queue
        ################################################################
        if self.torrent.connection.timeout and utility.config.Read('timeouttracker') != "oo":
            try:
                if self.numerrortracker > utility.config.Read('timeouttracker', "int"):
                    self._ReducePrioandForceQueue()
            except:
                pass
        
    def _infomsg(self, message):
        self.torrent.changeMessage(message, "status")
        
    def _ReducePrioandForceQueue(self):
        currentprio = self.torrent.prio
        if currentprio < 4:      #prio is not lowest
            self.torrent.changePriority(currentprio + 1)     #lower 1 prio

        self._queueMe()

    def _countSeedingTime(self):
        now = time()
        if self.seedingtimelastcheck is None:
            lastcheck = now
        else:
            lastcheck = self.seedingtimelastcheck
        timelapse = now - lastcheck
        
        self.torrent.connection.seedingtime += timelapse
        
        # upload time option
        time1 = 999999999999999
        if self.torrent.connection.getTargetSeedingTime() > 0:
            time1 = self.torrent.connection.getTargetSeedingTime() - self.torrent.connection.seedingtime
        # upload ratio option
        time2 = 999999999999999
        if self.torrent.connection.getSeedOption('uploadratio') != "0":
            if self.meanrate['up'] > 0:
                if self.torrent.files.downsize == 0.0 : 
                    down = self.torrent.files.floattotalsize
                else:
                    down = self.torrent.files.downsize
                up = self.torrent.files.upsize
                ratio = float(self.torrent.connection.getSeedOption('uploadratio'))
                required = ((ratio / 100.0) * down) - up
                newseedingtimeleft = required / self.meanrate['up']
                delta = max(newseedingtimeleft/10, 2)
                if abs(self.torrent.connection.seedingtimeleft - newseedingtimeleft) > delta:
                    # If timer value deviates from theoretical value by more then 10%, reset it to theoretical value
                    time2 = newseedingtimeleft
                else:
                    # Keep on timing
                    time2 = self.torrent.connection.seedingtimeleft - timelapse
                if time2 < 0.1:
                    time2 = 0.1
            else:
                # Set to 366 days (= infinite)
                time2 = 999999999999999
        if time1 < time2:
            self.torrent.connection.seedingtimeleft = time1
        else:
            self.torrent.connection.seedingtimeleft = time2

        self.seedingtimelastcheck = now

    def _TerminateUpload(self):
        # Terminate process
        ####################################################
        # change:   5:Progress  6:BT Status
        # clear : 8:ETA 10:DLSpeed 11:ULspeed
        #         14:#seed 15:#peer 16:#copie 17:peer avg
        #         20:total speed
        #####################################################
        self.torrent.status.completed = True
        self.progress = 100.0
        
        self.torrent.connection.stopEngine()
        
        utility.queue.updateAndInvoke()

    def unfinished(self):
        # torrent was completed but now isn't
        self.invokeLater(self._unfinished)

    def _unfinished(self):
        self.seed = False
        self.torrent.status.completed = False
        self.torrent.files.updateProgress()
        if self.torrent.completedTime:
            self.torrent.completedTime = 0

        # Update cols 5, 6, 8, 10
        self.torrent.updateColumns([COL_PROGRESS, 
                                    COL_BTSTATUS, 
                                    COL_ETA, 
                                    COL_DLSPEED,
                                    COL_COMPLETED_ON,
                                    COL_SEEDING_TIME])

        utility.queue.updateAndInvoke()

    def finished(self):
        self.invokeLater(self._finished)
        
    def _finished(self):
        self.seed = True
        
        # seeding process
        ####################################################
        # change:   5:Progress  6:BT Status
        # clear :   8:ETA 10:DLSpeed  
        #####################################################
        self.torrent.status.completed = True
        self.progress = 100.0
        self.torrent.files.updateProgress()
        if self.torrent.completedTime == 0:
            self.torrent.completedTime = time()

        # Check if done uploading
        if self.torrent.status.isDoneUploading():
            utility.frame.tbicon.ShowBalloonTip("Seeding Task Completed", self.torrent.getTitle())
            self._TerminateUpload()
        # Check if just got completed
        elif self.torrent.status.getStatusText() == _("working") and utility.frame.tbicon is not None:
            utility.frame.tbicon.ShowBalloonTip("Download Task Completed", self.torrent.getTitle())

        # Move files
        if self.torrent.status.getStatusText() == _("working") and \
           utility.config.Read('movecompleted', "boolean") and \
           not utility.config.Read('movecompletedonclear', "boolean") and \
           utility.config.Read('defaultmovedir'):

            paused = False
            if self.torrent.status.isActive(pause = False):
                paused = self.torrent.actions.pause()
            self.torrent.files.move()
            if paused:
                self.torrent.actions.pause(True)
            
        # Update cols 5, 6, 8, 10
        self.torrent.updateColumns([COL_PROGRESS, 
                                    COL_BTSTATUS, 
                                    COL_ETA, 
                                    COL_DLSPEED,
                                    COL_COMPLETED_ON])


        utility.queue.updateAndInvoke()
            
    def failed(self):
        self.actionhandlerCallback(STATUS_STOP, self.torrent)
                
    def _queueMe(self):       
        """
        Only queue if other things are waiting
        that would start up by queuing this torrent
        """
        # See what the next torrent to start would be if we queued
        # this torrent
        inactivetorrents = utility.queue.getInactiveTorrents(1)
        if not inactivetorrents:
            return
        
        nexttorrent = inactivetorrents[0]
        
        # See if this torrent would be started if queued
        queuethis = False
        if (nexttorrent.prio < self.torrent.prio):
            queuethis = True
        elif (nexttorrent.prio == self.torrent.prio) and (nexttorrent.listindex < self.torrent.listindex):
            queuethis = True

        if queuethis:
            self.btstatus = _('queued')
            self.actionhandlerCallback(STATUS_QUEUE, self.torrent)
            
    def actionhandlerCallback(self, action, torrent):
        self.invokeLater(self.onActionhandler, [action, torrent])

    def onActionhandler(self, action, torrent):
        if action == STATUS_QUEUE:
            utility.actionhandler.procQUEUE([torrent])
        else:
            utility.actionhandler.procSTOP([torrent])
